import React from 'react'
import levenshteinDistance from './algorithm'
import './App.css'
import * as d3 from 'd3'
import 'antd/dist/antd.css'
import { Form, Input, Button, Slider } from 'antd'
import $ from 'jquery'


class App extends React.Component {

  _fromValue = 'helllo word'
  _toValue = 'Hello world!'
  _calcResult = levenshteinDistance(this._fromValue, this._toValue)
  _animationSpeed = 300
  _canInitSvg = true
  _steping = true
  _currentStep = 0
  _currentIndex = 0
  _fromLength = this._fromValue.length
  _stop = false
  _playend = false
  _isMobile = false
  _lastMobile = null
  _lastRight = null
  _lastTop = null
  _initRight = 10
  _initTop = 10
  _clickFlag = false
  _lastX = null
  _lastY = null
  _moveFlag = false

  resize() {
    if (document.body.clientWidth < 1000) {
      this._isMobile = true
    }
    else {
      this._isMobile = false
    }
    if (this._lastMobile === null) {
      this._lastMobile = this._isMobile
    } else
      if (this._lastMobile !== this._isMobile) {
        this._stop = true
        this._steping = false
        this._currentIndex = 0
        this._currentStep = 0
        setImmediate(() => {
          this._canInitSvg = true
          this._lastMobile = this._isMobile
          this.setState({
          }, this.initSvg)

        })
      }
  }
  constructor(props) {
    super(props)
    this.state = {
      fromIsError: true,
      toIsError: true,
      sliderValue: 1000,
      step: [],
      showStep: false
    }
    this.resize()
    window.addEventListener('resize', () => {
      this.resize()
    })
  }
  componentDidMount() {
    this.initSvg()
    this._lastRight = this._initRight
    this._lastTop = this._initTop
    this.setState({
      top: this._initTop,
      right: this._initRight
    })
  }
  play() {
    if (this._canInitSvg) {
      if (this._playend) {
        this._playend = false
        this.initSvg()
      }
      this._stop = false
      this._play()
    }
  }
  async _play() {
    if (!this._stop) {
      await this.nextStep()
      setImmediate(() => this._play())
    }

  }
  validateFrom() {
    if (this._from) {
      const text = this._from.input.value
      if (text && text !== '' && text.length <= 30) {
        this.setState({
          fromIsError: false
        })
      } else {
        this.setState({
          fromIsError: true
        })

      }
    }
  }
  validateTo() {
    if (this._to) {
      const text = this._to.input.value
      if (text && text !== '' && text.length <= 30) {
        this.setState({
          toIsError: false
        })
      } else {
        this.setState({
          toIsError: true
        })

      }
    }
  }
  canCalc() {
    return !this.state.fromIsError && !this.state.toIsError
  }
  onCalc() {
    this._stop = true;
    setImmediate(() => {
      this._fromValue = this._from.input.value
      this._toValue = this._to.input.value
      this._fromLength = this._fromValue.length
      this._calcResult = levenshteinDistance(this._fromValue, this._toValue)
      this._currentStep = 0
      this._currentIndex = 0
      this.initSvg()
    })
  }
  initSvg() {
    if (this._canInitSvg) {
      this._canInitSvg = false
      this._steping = true
      this.setState({
        step: []
      }, async () => {
        const svg = this._isMobile ? d3.select(this._svg2) : d3.select(this._svg)
        svg.selectAll('text').nodes().forEach(it => it.remove())
        svg.select('rect').remove()
        svg.append('rect').attr('x', 8).attr('y', 50).attr('width', 32).attr('height', 95).attr('opacity', 0).attr('stroke', 'red').attr('fill', 'transparent').attr('stroke-width', 5)
        await this.initUpdateWord()
        d3.select('rect').transition().duration(this._animationSpeed).attr('opacity', 1).ease(d3['easeLinear']).end().then(() => {
          this._steping = false
          this._canInitSvg = true
        })

      })
    }


  }
  initUpdateWord() {
    return new Promise(resolve => {
      const svg = this._isMobile ? d3.select(this._svg2) : d3.select(this._svg)
      this._fromValue.split('').forEach((value, index) => {
        svg.append("text").attr('x', 40 * index + 25).attr('text-anchor', 'middle').attr('font-family', 'FiraCode').attr('y', 110).attr('stroke', 'black').attr('font-size', 30).attr('opacity', 0).text(value)
      })
      this._toValue.split('').forEach((value, index) => {
        svg.append("text").attr('x', 40 * index + 25).attr('text-anchor', 'middle').attr('font-family', 'FiraCode').attr('y', 150).attr('stroke', 'black').attr('font-size', 30).attr('opacity', 0).text(value)
      })
      svg.selectAll("text").nodes().forEach(it => {
        d3.select(it).transition().duration(this._animationSpeed).attr('y', d3.select(it).attr('y') - 20).attr('opacity', 1).ease(d3['easeLinear']).end().then(resolve)
      })
    })
  }
  replaceAnim(ele, m) {
    return new Promise(resolve => {
      d3.select(ele).transition().duration(this._animationSpeed).attr('y', 90).attr('opacity', 1).ease(d3.easeSin)
      d3.select(m).transition().duration(this._animationSpeed).attr('y', 120).attr('opacity', 0).ease(d3.easeSin).end().then(() => {
        m.remove()
        resolve()
      })


    })
  }
  matchAnim(e1, e2) {
    return new Promise(resolve => {
      d3.select(e2).transition().duration(this._animationSpeed).attrTween('fill', () => {
        return function (t) {
          const m = d3.interpolateNumber(0, 128)(t)
          return `rgb(0,${m},0)`
        }
      }).attrTween('stroke', () => {
        return function (t) {
          const m = d3.interpolateNumber(0, 128)(t)
          return `rgb(0,${m},0)`
        }
      }).end().then(() => {
        resolve()
      });
      d3.select(e1).transition().duration(this._animationSpeed).attrTween('fill', () => {
        return function (t) {
          const m = d3.interpolateNumber(0, 128)(t)
          return `rgb(0,${m},0)`
        }
      }).attrTween('stroke', () => {
        return function (t) {
          const m = d3.interpolateNumber(0, 128)(t)
          return `rgb(0,${m},0)`
        }
      }).end().then(() => {
        resolve()
      });

    })
  }
  deleteAnim(del, arr) {
    return new Promise(resolve => {
      arr.forEach(it => {
        d3.select(it).transition().duration(this._animationSpeed).attr('x', d3.select(it).attr('x') - 40).ease(d3.easeSin)
      })
      d3.select(del).transition().duration(this._animationSpeed)
        .attr('y', 120).attr('opacity', 0).ease(d3.easeSin).end().then(() => {
          del.remove()
          resolve()
        })
    })
  }
  insertAnim(ins, arr) {
    return new Promise(resolve => {
      arr.forEach(it => {
        d3.select(it).transition().duration(this._animationSpeed).attr('x', Number.parseInt(d3.select(it).attr('x')) + 40).ease(d3.easeSin)
      })
      d3.select(ins).transition().duration(this._animationSpeed)
        .attr('y', 90).attr('opacity', 1).ease(d3.easeSin).end().then(() => {
          resolve()
        })
    })
  }
  async nextStep() {
    if (this._canInitSvg) {
      if (this._playend) {
        this._playend = false;
        await this.initSvg();
      }
      if (!this._steping) {
        this._steping = true
        if (this._currentStep < this._calcResult.count) {
          const step = this._currentStep
          const result = this._calcResult.result[step]
          this.setState({
            step: [...this.state.step, result]
          }, () => {
            const lastScrollTop = this._ol.scrollTop
            d3.select(this._ol).transition().duration(this._animationSpeed).tween('ol', () => {
              return (t) => {
                this._ol.scrollTop = d3.interpolateNumber(lastScrollTop, this._ol.scrollHeight)(t)
              }
            })
          })
          const type = result.type
          if (type === 'match') {
            const texts = d3.selectAll('text').nodes()
            const e1 = texts[this._currentIndex];
            const e2 = texts[this._currentIndex + this._fromLength];
            await this.matchAnim(e1, e2)
            this._currentIndex++

          } else if (type === 'replace') {
            const t = result.to
            const texts = d3.selectAll('text').nodes()
            const m = texts[this._currentIndex]
            const e2 = texts[this._currentIndex + this._fromLength];
            const ele = document.createElementNS('http://www.w3.org/2000/svg', 'text')
            $(ele).attr('x', 40 * this._currentIndex + 25).attr('text-anchor', 'middle').attr('font-family', 'FiraCode').attr('y', 60).attr('stroke', 'black').attr('font-size', 30).attr('opacity', 0).text(t)
            $(m).before(ele)
            await this.replaceAnim(ele, m)
            await this.matchAnim(ele, e2)
            this._currentIndex++
          } else if (type === 'insert') {
            const texts = d3.selectAll('text').nodes()
            const arr = texts.slice(this._currentIndex, this._fromLength)
            const e2 = texts[this._currentIndex + this._fromLength];
            const t = result.value
            const m = d3.selectAll('text').nodes()[this._currentIndex]
            const ele = document.createElementNS('http://www.w3.org/2000/svg', 'text')
            $(ele).attr('x', 40 * this._currentIndex + 25).attr('text-anchor', 'middle').attr('font-family', 'FiraCode').attr('y', 60).attr('stroke', 'black').attr('font-size', 30).attr('opacity', 0).text(t)
            $(m).before(ele)
            await this.insertAnim(ele, arr)
            await this.matchAnim(ele, e2)
            this._fromLength++
            this._currentIndex++
          } else if (type === 'delete') {
            const texts = d3.selectAll('text').nodes()
            const del = texts[this._currentIndex];
            const arr = texts.slice(this._currentIndex + 1, this._fromLength)
            await this.deleteAnim(del, arr)
            this._fromLength--
          }
          this._currentStep++
          if (this._currentStep < this._calcResult.count)
            d3.select('rect').transition().duration(this._animationSpeed).attr('x', 8 + this._currentIndex * 40).ease(d3['easeSin']).end().then(() => {
              this._steping = false
            })
          else {
            d3.select('rect').transition().duration(this._animationSpeed).attr('opacity', 0).ease(d3['easeSin']).end().then(() => {
              this._steping = false
              this._stop = true
              this._playend = true;
              this._currentIndex = 0
              this._currentStep = 0
              this._fromLength = this._fromValue.length

            })
          }
        }
      }
    }
  }
  render() {
    return <div>{(
      this._isMobile ? (
        <div>
          <h1 style={{ textAlign: 'center', fontSize: 'calc(3vw + 20px)', marginTop: 60 }}><span style={{ fontFamily: 'FiraCode' }}>Levenshtein</span> 编辑距离</h1>
          <Form style={{ textAlign: 'center', marginTop: 60 }} layout="inline" onSubmit={() => this.onSubmit()}>
            <Form.Item validateStatus={this.state.fromIsError ? 'error' : 'success'} help={this.state.fromIsError ? '1-30个字符' : ''}>
              <Input style={{ width: 300 }} ref={value => this._from = value} onInput={() => { this.validateFrom() }} placeholder="From"></Input>
            </Form.Item>
            <div style={{ fontSize: 30, transform: this.state.fromIsError ? 'translateY(-10px)' : '', fontFamily: 'FiraCode', height: 40, lineHeight: '40px' }}>
              ⬇
          </div>
            <Form.Item validateStatus={this.state.toIsError ? 'error' : 'success'} help={this.state.toIsError ? '1-30个字符' : ''}>
              <Input style={{ width: 300 }} ref={value => this._to = value} onInput={() => { this.validateTo() }} placeholder="To"></Input>
            </Form.Item>
            <br />
            <Form.Item>
              <Button type="primary" htmlType="button" disabled={!this.canCalc()} onClick={() => this.onCalc()}>计算</Button>
            </Form.Item>
          </Form>
          <Form style={{ textAlign: 'center' }} layout="inline">
            <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
              <span>动画速度(ms)</span>
              <Slider style={{ marginLeft: 10, width: 200 }} min={16} max={1000} defaultValue={300} onAfterChange={v => this._animationSpeed = v}>
              </Slider>
            </div>
            <br />
            <Form.Item>
              <Button type="primary" title="连续播放" onClick={() => this.play()}>
                <svg width={15} height={15} version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 100 100">
                  <polygon points="10,10 70,50 10,90" fill="white" stroke="transparent" />
                </svg>
              </Button>
            </Form.Item>
            <Form.Item>
              <Button type="primary" title="单步查看" onClick={() => this.nextStep()}>&gt;</Button>
            </Form.Item>
          </Form>
          <div style={{
            overflowX: 'scroll',
            margin: 5,
            borderRadius: '8px',
          }}>
            <svg width={1200} height={160} viewBox="0 20 1200 160" className="svg" ref={svg => this._svg2 = svg} version="1.1" xmlns="http://www.w3.org/2000/svg">
            </svg>
          </div>
        </div>
      )
        : (
          <div>
            <h1 style={{ textAlign: 'center', fontSize: '3em', marginTop: 60 }}><span style={{ fontFamily: 'FiraCode' }}>Levenshtein</span>编辑距离</h1>
            <Form style={{ textAlign: 'center', marginTop: 60 }} layout="inline" onSubmit={() => this.onSubmit()}>
              <Form.Item validateStatus={this.state.fromIsError ? 'error' : 'success'} help={this.state.fromIsError ? '1-30个字符' : ''}>
                <Input style={{ width: 300 }} ref={value => this._from = value} onInput={() => { this.validateFrom() }} placeholder="From"></Input>
              </Form.Item>
              <div style={{ display: 'inline-block', fontFamily: 'FiraCode', height: 40, lineHeight: '40px' }}>
                ==>
          </div>
              <Form.Item validateStatus={this.state.toIsError ? 'error' : 'success'} help={this.state.toIsError ? '1-30个字符' : ''}>
                <Input style={{ width: 300, marginLeft: 16 }} ref={value => this._to = value} onInput={() => { this.validateTo() }} placeholder="To"></Input>
              </Form.Item>
              <Form.Item>
                <Button type="primary" htmlType="button" disabled={!this.canCalc()} onClick={() => this.onCalc()}>计算</Button>
              </Form.Item>
            </Form>
            <Form style={{ textAlign: 'center' }} layout="inline">
              <Form.Item label="动画速度(ms)">
                <Slider style={{ width: 200 }} min={16} max={1000} defaultValue={300} onAfterChange={v => this._animationSpeed = v}>
                </Slider>
              </Form.Item>
              <Form.Item>
                <Button type="primary" title="连续播放" onClick={() => this.play()}>
                  <svg width={15} height={15} version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 100 100">
                    <polygon points="10,10 70,50 10,90" fill="white" stroke="transparent" />
                  </svg>
                </Button>
              </Form.Item>
              <Form.Item>
                <Button type="primary" title="单步查看" onClick={() => this.nextStep()}>&gt;</Button>
              </Form.Item>
            </Form>
            <div style={{
              margin: 5,
              borderRadius: '8px',
              overflowX: 'auto'
            }}>
              <svg
                width={this.state.svgWidth} height={160} viewBox="0 20 1200 160" className="svg" ref={svg => this._svg = svg} version="1.1" xmlns="http://www.w3.org/2000/svg">
              </svg>
            </div>
          </div>
        )

    )}

      <div style={{
        borderRadius: 8,
        boxShadow: '1px 1px 8px gray',
        width: 130,
        position: 'fixed',
        right: this.state.right,
        top: this.state.top,
        backgroundColor: 'white',
        height: this.state.showStep ? '' : '31px',
        overflow: this.state.showStep ? '' : 'hidden',
        overflowY: this.state.showStep ? 'auto' : 'hidden'

      }}>
        <h4 onClick={() => {
          if (!this._moveFlag) {
            this.setState(({
              showStep: !this.state.showStep
            }))
          }
        }} onPointerDown={e => {
          this._clickFlag = true
          this._moveFlag = false
          e.currentTarget.setPointerCapture(e.pointerId)
          this._lastX = e.clientX
          this._lastY = e.clientY
        }} onPointerUp={e => {
          e.currentTarget.style.cursor = "pointer"
          e.currentTarget.releasePointerCapture(e.pointerId)
          this._lastTop = this.state.top
          this._lastRight = this.state.right
          this._clickFlag = false
        }}
          onPointerMove={e => {
            if (this._clickFlag) {
              const delta1 = Math.abs(e.clientX - this._lastX)
              const delta2 = Math.abs(e.clientY - this._lastY)
              if (delta1 > 5 && delta2 > 5 && !this._moveFlag) {
                this._moveFlag = true
                e.currentTarget.style.cursor = "move"
              }
              this.setState({
                right: this._lastRight - e.clientX + this._lastX,
                top: this._lastTop + e.clientY - this._lastY,
              })

            }


          }} style={{ padding: '5px 0', userSelect: 'none', cursor: 'pointer' }}>步骤
        <span className={`dir ${this.state.showStep ? 'rotate' : ''}`}>
            &nbsp;↓&nbsp;
        </span></h4>
        <hr />
        <ol style={{
          paddingInlineStart: 30,
          maxHeight: 250,
          overflow: this.state.showStep ? '' : 'hidden',
          overflowY: this.state.showStep ? 'auto' : 'hidden',
          textAlign: 'left',
          paddingTop: 10
        }} ref={ol => this._ol = ol}>
          {
            this.state.step.map((v, i) => {
              return <li style={{ fontSize: 12, padding: '5px 0' }} key={i}>
                {
                  (() => {
                    if (v.type === 'delete') {
                      return `删除字符'${v.value}'`
                    } else if (v.type === 'insert') {
                      return `插入字符'${v.value}'`
                    } else if (v.type === 'replace') {
                      return `替换字符'${v.from}'为'${v.to}'`
                    } else {
                      return `字符'${v.value}'已匹配`
                    }
                  })()
                }
              </li>
            })
          }
        </ol>

      </div>
    </div>

  }
}

export default App
